iT邦幫忙

2024 iThome 鐵人賽

DAY 25
0

昨日詠唱出一個小框架,今天我先試著延伸 action_client 包含到那框架的網頁部份,讓使用者可以從網頁端傳座標資訊回到代理人端,然後再讓代理人用座標著點和伺服器溝通。不確定能夠成功到什麼程度就是。

給新朋友:從 Day 24 開始,這個系列就是每天且戰且走的狀態了。有多少打多少。有點悲傷的是,Day 23 放下去運行的收集、訓練迴圈,稍早的時候熱當中斷,但也因禍得福發現,中間有接錯...導致沒有成功累積訓練結果。重啟了。

無獨有偶,公司的幾位同仁剛好在本週的讀書會分享了強化學習主題。唉,不確定是不是每個人都能夠從那些理論出發並且走到應許之地?不管是在講 value 或是在講 policy,都難免用圍棋當作例子,但...唉。

對照:讀譜程式

其實我仔細看了昨天的程式碼之後,發現它透過 std::sync::mpsc 函式庫幫忙傳遞兩組訊號給網頁前端,reloadclick。前者其實我也不確定需求,主要是因為,參考到現在的讀譜程式,可以發現,所開啟的網頁伺服器有一個 /game_state 的 route,是靜態的紀錄每一個著點,並且將它們轉譯到有 UI 意義的資訊。第 171 行結束設置部份的讀取之後,是整份棋譜每個標準回合的解譯,如(說來有趣,Day20剛好沒有解析內迴圈的內容)

        let mut route = vec![];
        for s in curr_node.borrow().properties[0].value.iter() { // 內迴圈,外面包一個大迴圈走過所有標準回合,而這個迴圈只走一個標準回合的每個著點。如果是圍棋的 SGF 的話,這裡的 value 就會只有一個,而不會需要用 iter 方法展開,但是疫途的一個回合有很多個著點。
            let is_marker = route.contains(s); // 標準回合裡面,標記階段的特別判定方法
            gs.steps.push(Step { // gs 即是 game state
                id: id, // id 是不斷累加的流水號,如下方
                pos: s.clone(), // s 是座標在 SGF 格式的字串模樣
                is_marker: is_marker,
                char1: side, // 陣營,直接以 'P' 或 'D' 字元顯示,代表疫病方或醫療方
                marker: if !is_marker {
                    0
                } else if side == 'D' { // 在聊到資料集的時候曾經提過考慮用 +/- 1 的值來代表標記但很慶幸沒那麼做;現在這裡只和 UI 有關,所以負號我用來當作顯示顏色的判斷。
                    1
                } else {
                    -1
                },
            });

            id = id + 1;
            if !is_marker { // 若不是 marker,就會被加入到 route 裡面去。
                route.push(s.clone());
            }
        }

這些 game_state 內容,會被製成 json 格式,讓 javascript 可以取用。在瀏覽器頁面使用 PageUp/Down 鍵盤事件控制的時候,其實是在移動這個流水號,使它顯示不同的狀態。

順著走的時候的狀態很明確可以更新,但是回顧先前著手、逆著走的時候,還是有些地方有 bug 我沒有修。比方說棋盤階段,可能短暫地經過某個敵方角色,然後令它的顯示暫時消失之類。

回到先前提到的 reload 訊號。由於讀譜程式是靜態收集這些遊戲狀態,但如果是要對弈的話,總會讓這個紀錄變成需要隨著遊戲進行而增加的一個東西。還有另外一個考量則是,現在的遊戲伺服器可以提供多局對弈功能,我不確定網頁伺服器的呈現應該怎麼做。總之,我對網頁設計實在沒有什麼功力,想說大概 reload 治百病?也還不確定用不用得到就是。

其實我後續還和它(o1-mini)聊了一下,說我會希望再加第三組訊號,讓我可以從後端這裡處理的事件主迴圈裡面,讓前端也能夠模擬出鍵盤事件 PageUp。這也是很單純的想法,就是如果前端已經取得了新的 game_state,那依據我原本的讀譜程式的邏輯,也應該要能夠走到最新的 id 的狀態去才對。然而它著實教育了我一番,基於安全性的考量,不應該有手法可以直接模擬鍵盤事件,然後給了我一些捲動捲軸的程式碼替代,問題是我也不知道那些有沒有用。且先擱置。

銜接訊號

將 ChatGPT 老師的框架程式碼套過來行動代理人(action_client)這邊,做成這個 patch。可以抓到座標沒問題,但是因為現在前端只有一個範例用的 2x2 方格,所以還不能真的拿來對弈。幾個遭遇到的狀況在這裡簡單描述一下。

首先是它原本取得滑鼠點擊事件的方式,是用非阻塞(non-blocking)的作法,外層套上固定時間倒數、時間到則跳出迴圈的作法。我將之改成阻塞的作法,這樣就會等著玩家下了之後才動作,

-            } else if status == "Ix01".as_bytes() || status == "Ix0b".as_bytes() {
-                stream.write(&[c])?;
-            } else if status == "Ix03".as_bytes() {
-                // the main play of this round
-                stream.write(&[c])?;
             } else {
-                // Error?
+                if status == "Ix01".as_bytes() || status == "Ix0b".as_bytes() {
+                    // the normal middle moves
+                } else if status == "Ix03".as_bytes() {
+                    // the main play of this round
+                } else {
+                    // Error?
+                }
+
+                if let Ok((x, y)) = click_rx.recv() {
+                    println!("Received click at ({}, {}) from web interface.", x, y);
+                }
                 stream.write(&[c])?;
             }

本來是分成三組判斷區塊,但因為沒有其他需要處理的事情,所以三個區塊都是下了就對了stream.write(&[c])?;),但當然,目前這個候選著手(c)是一個亂選的值,還沒有把 (x,y) 串過去。

再來是,剛串起來可以編之後,在網頁端怎麼點都沒反應。後來才發現我座標兩邊型別沒統一。怪了 Rust 對這個不是很敏感嗎?我還不是從看 code 發現,而是從瀏覽器開 F12 之後看到一堆 500 Internal Server Error,盲猜一個結果改對了。需要統一的是 main.rs

let (click_tx, click_rx): (Sender<(i32, i32)>, Receiver<(i32, i32)>) = mpsc::channel();

web_server.rs

#[derive(Deserialize)]
struct ClickData {
    x: i32,
    y: i32,
}

最後一個是 ChatGPT 老師縱使在我的威脅之下,還是出了相依性錯誤。錯誤的部份就是這裡的 Deserialize 造成的。它給我的是

use serde::Deserialize;

但其實應該要是

use serde_derive::Deserialize;

Anyway,現在這樣,我就可以繼續串了吧?


上一篇
對戰網頁實作:先詠唱再說
下一篇
對戰網頁實作:解決輸入
系列文
DeltaPathogen:國產雙人不對稱抽象棋「疫途」之桌遊 AI 實戰30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言